﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Text.RegularExpressions;
using St0rm.IRCPro.Common;

namespace St0rm.IRCPro.Client {
    /// <summary>
    /// A Client to the IRC protocol
    /// </summary>
    public class Client {

        #region Regular Expressions
        static Regex rRawEvents = new Regex(@"^:(\S+) (\d+) (.*)");
        //static Regex rServerMessages = new Regex(@"^:(\S+) (.*)");
        static Regex rRawMessages = new Regex(@"(\d+) (.*)");
        static Regex rUserInfo = new Regex(@"([^!]+![^@]+@\S+)");
        static Regex rPrivMessage = new Regex(@"^:?(\S+) PRIVMSG (\S+) :(.*)");
        static Regex rJoin = new Regex(@"^:?" + rUserInfo + @" JOIN :?(.*)");
        static Regex rChanMode = new Regex(@"^:?(\S+) MODE (\S+) (.*)");
        static Regex rNames = new Regex("([^ !]+)");
        #endregion

        #region Member Variables
        private string _nick = null;
        private string _username = null;
        private string _hostname = "St0rm.net";
        private string _realname = null;
        private string _version = "St0rm.net IRCPro Library for .NET";
        private bool _connected = false;
        private bool _verbose = false;
        private bool _strictNames = false;
        private Encoding _encoding = Encoding.ASCII;
        private int _queueTrigger = 0;
        private int _queueDelay = 0;
        private int _queueLimit = 0;
        private int _queueCycle = 0;
        private bool _queueInProcess = false;
        private Queue<string> _msgQueue = new Queue<string>();
        private System.Timers.Timer _queueTimer = new System.Timers.Timer();
        private TcpClient _socket = null;
        private StreamWriter _writer = null;
        private StreamReader _reader = null;
        private ServerInfo _server = new ServerInfo();
        #endregion

        #region Delegates
        /// <summary>
        /// Handles single lines from the server
        /// </summary>
        /// <param name="line">Line to handle</param>
        public delegate void HandleSingleLine(string line);
        /// <summary>
        /// Handles a channel message
        /// </summary>
        /// <param name="sender">User who sent the message</param>
        /// <param name="target">Channel the message was sent to</param>
        /// <param name="message">Message sent</param>
        public delegate void HandleChannelMessage(string sender, Channel target, string message);
        /// <summary>
        /// Handles a private message
        /// </summary>
        /// <param name="sender">User who sent the message</param>
        /// <param name="message">Message sent</param>
        public delegate void HandlePrivateMessage(string sender, string message);
        /// <summary>
        /// Handles events where a user joins a channel
        /// </summary>
        /// <param name="user">User who joined the channel</param>
        /// <param name="channel">Channel joined by user</param>
        public delegate void HandleJoins(User user, Channel channel);
        #endregion

        #region Events
        /// <summary>
        /// Triggers when any line is received from the server BEFORE it is processed by the library
        /// </summary>
        public event HandleSingleLine OnRawLine;
        /// <summary>
        /// Triggers when a NOTICE AUTH its sent
        /// </summary>
        public event HandleSingleLine OnNoticeAuth;
        /// <summary>
        /// Triggers when a PING is sent, line is the source
        /// </summary>
        public event HandleSingleLine OnPing;
        /// <summary>
        /// Triggers when the socket connects and an RPL_WELCOME is received from the server
        /// </summary>
        public event Action OnConnect;
        /// <summary>
        /// Triggers when a user sends a message to a channel you are on
        /// </summary>
        public event HandleChannelMessage OnChannelMessage;
        /// <summary>
        /// Triggers when a user sends you a private message
        /// </summary>
        public event HandlePrivateMessage OnPrivateMessage;
        /// <summary>
        /// Triggers when someone joins a channel you are on
        /// </summary>
        public event HandleJoins OnJoin;
        #endregion

        #region Properties
        /// <summary>
        /// Get whether or not this client is ready to connect to an IRC Server
        /// </summary>
        public bool IsReady {
            get {
                return !String.IsNullOrEmpty(_nick) && !String.IsNullOrEmpty(_username) && !String.IsNullOrEmpty(_hostname) && !String.IsNullOrEmpty(_realname);
            }
        }
        /// <summary>
        /// Gets if the Socket is connected to the Server 
        /// </summary>
        public bool IsSockConnected {
            get {
                return _socket != null && _socket.Connected;
            }
        }
        /// <summary>
        /// Gets if the bot has successfully athenticated with the server and has gotten RPL_WELCOME
        /// </summary>
        public bool IsConnected {
            get {
                return _connected;
            }
            
        }

        /// <summary>
        /// Gets or sets the clients nickname. Setting it while connected will send NICK to the server
        /// </summary>
        public String Nick {
            get {
                return _nick;
            }
            set {
                if (IsSockConnected)
                    Send("NICK {0}", value);
                else
                    _nick = value;
            }
        }
        /// <summary>
        /// Gets or sets the clients username for use in the USER command only
        /// </summary>
        public string Username {
            get {
                return _username;
            }
            set {
                _username = value;
            }
        }
        /// <summary>
        /// Gets or sets the clients realname for use in the USER command only
        /// </summary>
        public string Realname {
            get {
                return _realname;
            }
            set {
                _realname = value;
            }
        }

        /// <summary>
        /// Gets or sets the CTCP Version reply for the library
        /// </summary>
        public string Version {
            get {
                return _version;
            }
            set {
                _version = value;
            }
        }

        /// <summary>
        /// Gets the Encoding used in this connection
        /// </summary>
        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }
        }

        /// <summary>
        /// Gets and sets Strict Names
        /// If enable, forces the bot to request NAMES after every mode change to ensure latest levels.
        /// Defaults to false
        /// </summary>
        public bool StrictNames
        {
            get
            {
                return _strictNames;
            }
            set
            {
                _strictNames = value;
            }
        }

        /// <summary>
        /// Gets the ServerInfo for the server currently connected
        /// </summary>
        public ServerInfo Server {
            get {
                if (!IsConnected)
                    throw new NotConnectedException();
                return _server;
            }
        }

        /// <summary>
        /// Turns verbose output on or off
        /// </summary>
        public bool Verbose {
            get {
                return _verbose;
            }
            set {
                _verbose = value;
            }
        }

        #endregion

        /// <summary>
        /// Initializes a new Client with the given values
        /// </summary>
        /// <param name="nick">Nick of the client</param>
        /// <param name="username">Username for the USER command</param>
        /// <param name="realname">Realname for the USER command</param>
        public Client(string nick, string username, string realname) {
            _nick = nick;
            _username = username;
            _realname = realname;
            
        }
        /// <summary>
        /// Initializes a new Client with no pre-set values
        /// </summary>
        public Client() { }

        /// <summary>s
        /// Connects to an IRC server. If fails and returns false
        /// </summary>
        /// <param name="server">Server address to connect to</param>
        /// <param name="port">Port to connect on, default 6667</param>
        public void Connect(string server, int port) {
            Connect(server, port, null, Common.IRCEncodings.ASCII);
        }
        /// <summary>
        /// Connects to an IRC server. If fails and returns false
        /// </summary>
        /// <param name="server">Server address to connect to</param>
        /// <param name="port">Port to connect on, default 6667</param>
        /// <param name="pass">Password to send, if any</param>
        public void Connect(string server, int port, string pass)
        {
            Connect(server, port, pass, Common.IRCEncodings.ASCII);
        }
        /// <summary>
        /// Connects to an IRC server. If fails and returns false
        /// </summary>
        /// <param name="server">Server address to connect to</param>
        /// <param name="port">Port to connect on, default 6667</param>
        /// <param name="pass">Password to send, if any</param>
        /// <param name="encoding">Encoding to use</param>
        public void Connect(string server, int port, string pass, Common.IRCEncoding encoding) {
            if (!_queueTimer.Enabled)
            {
                _queueTimer.Interval = 20;
                _queueTimer.Elapsed += (x, y) =>
                    {
                        if (_queueTrigger > 0)
                        {
                            _queueCycle++;
                            if (_queueInProcess || _msgQueue.Count > _queueTrigger)
                            {
                                if (_queueTimer.Interval * _queueCycle >= _queueDelay)
                                {
                                    lock (_msgQueue)
                                    {
                                        Write(_msgQueue.Dequeue());
                                        _queueCycle = 0;
                                        _queueInProcess = _msgQueue.Count > 0;
                                    }
                                }
                            }
                            else
                            {
                                lock (_msgQueue)
                                {
                                    foreach (string message in _msgQueue)
                                        Write(message);
                                    _msgQueue.Clear();
                                    _queueCycle = 0;
                                }
                            }
                        }
                        else if (_msgQueue.Count > 0)
                        {
                            lock (_msgQueue)
                            {
                                foreach (string message in _msgQueue)
                                    Write(message);
                                _msgQueue.Clear();
                            }
                        }
                    };
                _queueTimer.Start();
            }
            lock (_msgQueue) // Keep lock for rest of connect, so no one else can add to it until we have connected
            {
                _msgQueue.Clear();
                if (!IsReady)
                {
                    throw new NotReadyException();
                }
                else if (IsSockConnected)
                {
                    Send("QUIT :New connection");
                    _connected = false;
                    _socket.Close();
                }
                _encoding = (Encoding)encoding;
                _socket = new TcpClient();

                _socket.Connect(server, port);

                // ALREADYREGISTRED NEEDMOREPARAMS NONICKNAMEGIVEN NICKNAMEINUSE ERRONEUSNICKNAME NICKCOLLISION

                if (!String.IsNullOrEmpty(pass))
                    Send("PASS {0}", pass);
                Send("NICK {0}", Nick);
                Send("USER {0} {1} {2} :{3}", Username, _hostname, server, Realname);

                string line = null;
                bool continueRead = true;
                _reader = new StreamReader(_socket.GetStream(), _encoding);
                while (continueRead && (line = _reader.ReadLine()) != null)
                {
                    if (OnRawLine != null)
                        OnRawLine(line);
                    if (line.StartsWith("NOTICE AUTH") && OnNoticeAuth != null)
                        OnNoticeAuth(line.Substring(line.IndexOf(':') + 1));
                    else if (line.StartsWith("PING "))
                    {
                        string source = line.Substring(line.IndexOf(':') + 1);
                        if (OnPing != null)
                            OnPing(source);
                        Send("PONG :{0}", source);
                    }
                    else if (rRawEvents.IsMatch(line))
                    {
                        int raw = int.Parse(rRawEvents.Match(line).Groups[2].Value);
                        if (raw == (int)Numeric.ERR_ALREADYREGISTERED)
                        {
                            throw new AlreadyRegisteredException();
                        }
                        else if (raw == (int)Numeric.ERR_NEEDMOREPARAMS || raw == (int)Numeric.ERR_NONICKNAMEGIVEN)
                        {
                            throw new FormatException("One or more required fields is not properly filled out");
                        }
                        else if (raw == (int)Numeric.ERR_ERRONEUSNICKNAME)
                        {
                            throw new FormatException("Nickname contains illegal characters");
                        }
                        else if (raw == (int)Numeric.ERR_NICKNAMEINUSE || raw == (int)Numeric.ERR_NICKNAMEINUSE)
                        {
                            throw new NickInUseException();
                        }
                        else if (raw == (int)Numeric.RPL_WELCOME)
                        {
                            _connected = true;
                            continueRead = false;
                            _server.Name = line.Substring(1, line.IndexOf(' '));
                            if (OnConnect != null)
                                OnConnect();
                        }
                    }

                }
            }
            new System.Threading.Thread(SocketReader).Start();


        }
        /// <summary>
        /// Sends raw data to the server
        /// </summary>
        /// <param name="data">Data to send</param>
        public void Send(string data) {
            if (_queueTrigger <= 0 || data.ToLower().StartsWith("pong") || data.ToLower().StartsWith("quit"))
                Write(data);
            else if (_queueTrigger > 0 && _msgQueue.Count < _queueLimit)
            {
                lock (_msgQueue)
                {
                    _msgQueue.Enqueue(data);
                }
            }
        }

        /// <summary>
        /// Sends raw data directly to the server
        /// </summary>
        /// <param name="data"></param>
        private void Write(string data)
        {
            if (IsSockConnected)
            {
                if (_writer == null)
                    _writer = new StreamWriter(_socket.GetStream(), _encoding);
                _writer.WriteLine(data);
                _writer.Flush();
                if (Verbose)
                    Console.WriteLine("-> {0}", data);
            }
            else
                throw new NotConnectedException();
        }

        /// <summary>
        /// Sends a message to all channels
        /// </summary>
        /// <param name="message"></param>
        /// <param name="multiTarget">If true, will send one multitarget privmsg. If false, will send multiple PRIVMSG's</param>
        public void AMsg(bool multiTarget, string message)
        {
            if (multiTarget)
            {
                StringBuilder chans = new StringBuilder();
                foreach (Channel c in Channel.Channels)
                {
                    if (c.Client == this)
                    {
                        chans.Append(c.Name);
                        chans.Append(',');
                    }
                }
                if (chans.Length > 0)
                {
                    Send("PRIVMSG {0} :{1}", chans.ToString(0, chans.Length - 1), message);
                }
            }
            else
            {
                foreach (Channel c in Channel.Channels)
                    if (c.Client == this)
                        Send("PRIVMSG {0} :{1}", c.Name, message);
            }
        }
        
        /// <summary>
        /// Sends a formatted message to al lchannels
        /// </summary>
        /// <param name="message"></param>
        /// <param name="multiTarget">If true, will send one multitarget privmsg. If false, will send multiple PRIVMSG's</param>
        /// <param name="format"></param>
        public void AMsg(bool multiTarget, string message, params object[] format)
        {
            AMsg(multiTarget, String.Format(message, format));
        }



        /// <summary>
        /// Sends formatted data to the server
        /// </summary>
        /// <param name="data">Data to send</param>
        /// <param name="format">Formatting</param>
        public void Send(string data, params object[] format) {
            Send(String.Format(data, format));
        }

        /// <summary>
        /// Enables message queueing for anti-spam
        /// </summary>
        /// <param name="trigger">The number of messages required before the lib begins delaying (0 to disable)</param>
        /// <param name="delay">The time between messages, >= 500 or defaults to 2500</param>
        /// <param name="limit">The max number of messages to queue >= 1 or defaults to 20</param>
        public void SetQueue(int trigger, int delay, int limit)
        {
            _queueTrigger = trigger;
            if (_queueDelay >= 500)
                _queueDelay = delay;
            else
                _queueDelay = 2500;
            if (limit > 0)
                _queueLimit = limit;
            else
                _queueLimit = 20;
        }

        private void SocketReader() {
            if (IsSockConnected && IsConnected) {
  
                string line = null;

                while (IsConnected && (line = _reader.ReadLine()) != null) {

                    if (OnRawLine != null)
                        OnRawLine(line);

                    if (rRawEvents.IsMatch(line)) {
                        int numeric = 0;
                        string message = rRawEvents.Match(line).Groups[3].Value;

                        if (!int.TryParse(rRawEvents.Match(line).Groups[2].Value, out numeric)) {
                            Log.Error(this, "Invalid numeric: {0}", line);                      
                        }
                        if (numeric == 0) { }
                        else if (numeric == (int)Numeric.RPL_ISUPPORT) {

                            if (message.Contains("PREFIX=")) {
                                _server.Prefix = message.Substring(message.IndexOf("PREFIX=") + 7,
                                    message.IndexOf(' ', message.IndexOf("PREFIX=") + 7) - (message.IndexOf("PREFIX=") + 7));
                       

                            }
                            if (message.Contains("CHANMODES="))
                            {
                                _server.Chanmodes = message.Substring(message.IndexOf("CHANMODES=") + 10,
                                    message.IndexOf(' ', message.IndexOf("CHANMODES=") + 10) - (message.IndexOf("CHANMODES=") + 10));
                               // Console.WriteLine("Set. {0}", _server.chanmodes);
                            }
                        }

                        else if (numeric == (int)Numeric.RPL_NAMREPLY)
                        {
                            // - :port80c.se.quakenet.org 353 aca20031 * #test :aca20031 duqugq levitation[A] @Q @Morph3us USCH Weeb mth`off|ramzi
                            Match m = Regex.Match(line,@"(\S+) :(.*)");
                            Channel c = Channel.GetChannel(m.Groups[1].Value, this);
                            if (c.IsInList) {
                             
                                foreach (Match match in rNames.Matches(m.Groups[2].Value)) {
                                    string name = match.Groups[1].Value;
                                    if (String.IsNullOrEmpty(name))
                                        continue;

                                    int i; // To hold the position the prefix ends
                                    for (i = 0; _server.Prefix.Substring(_server.Prefix.IndexOf(')') + 1).Contains(name.ToCharArray()[i]); i++) ;

                                    string prefixes = (i == 0 ? "" : name.Substring(0, i));
                                    string nick = name.Substring(i);

                                    User u = User.GetUser(nick, this);
                                    if (c.IsInList)
                                    {
                                        u.AddMe();
                                        c.AddUser(u);
                                        c.SetAccess(u, new ChannelAccess(prefixes, this));
                                    }



                                }
                            }
                        }
                    

                    }
                    else if (line.StartsWith("PING ")) {
                        string source = line.Substring(line.IndexOf(':')+1);
                        if (OnPing != null)
                            OnPing(source);
                        Send("PONG :{0}", source);
                     }
                    else if (rPrivMessage.IsMatch(line)) {
                        Match m = rPrivMessage.Match(line);
                        UserInfo sender = m.Groups[1].Value.ParseUserInfo();
                        User u = null;
                        if (sender.nick != null) {
                            u = User.GetUser(sender, this);
                        }
                        string target = m.Groups[2].Value;
                        string message = m.Groups[3].Value;
                        if (target.StartsWith("#")) {
                            Channel c = Channel.GetChannel(m.Groups[2].Value, this);

                            if (!c.IsInList) 
                                c.AddMe();

                            if (u != null && !u.IsInList)
                                u.AddMe();
                            
                            if (u != null)
                                c.AddUser(u);

                            if (OnChannelMessage != null)
                                OnChannelMessage(m.Groups[1].Value, c, message);
                        }
                        else {
                            if (OnPrivateMessage != null)
                                OnPrivateMessage(m.Groups[1].Value, message);
                        }
                        
                    }
                    else if (rJoin.IsMatch(line)) {
                        Match match = rJoin.Match(line);
                        UserInfo ui = match.Groups[1].Value.ParseUserInfo();
                        User u;
                        if (ui.nick.Equals(Nick)) {
                            u = User.GetMe(this);
                        }
                        else {
                            u = User.GetUser(ui.nick, this);
                        }

                        u.Host = ui.host;
                        u.Username = ui.user;

                        Channel c = Channel.GetChannel(match.Groups[2].Value, this);

                        if (!u.IsInList)
                            u.AddMe();
                        if (!c.IsInList)
                            c.AddMe();

                        c.AddUser(u);

                        if (OnJoin != null)
                            OnJoin(u, c);

                    }

                    else if (rChanMode.IsMatch(line)) {
                        try {
                            Match match = rChanMode.Match(line);
                            string setter = match.Groups[1].Value;
                            UserInfo info = setter.ParseUserInfo();
                            Channel chan = Channel.GetChannel(match.Groups[2].Value, this);
                            string modes = match.Groups[3].Value.Split(' ')[0];
                            string parameters = "";
                            if (match.Groups[3].Value.Split(' ').Length > 1)
                                parameters = match.Groups[3].Value.Substring(match.Groups[3].Value.IndexOf(' ') + 1);
                            if (info.nick != null) {
                                User usr = User.GetUser(info.nick, this);
                                usr.Host = info.host;
                                usr.Username = info.user;
                            }

                            //At this point, Setter is the userhost or server that set the modes
                            //modes are the modes set
                            //chan is the Channel object of the channel the modes were set on
                            //parameters is empty or the mode parameters
                            string[] chanmodetoks = Server.Chanmodes.Split(',');
                            string mode_list = chanmodetoks[0]; //These modes are list modes such as bans and invites +b +i
                            string mode_setunset = chanmodetoks[1]; //These modes need parameters to set and unset them
                            string mode_set = chanmodetoks[2]; //These modes need parameters to set only
                            string mode_none = chanmodetoks[3]; //These modes do not require parameters
                            string mode_user = _server.Prefix.Substring(1, _server.Prefix.IndexOf(')') -1); //user access modes 
                            string[] parametertoks = parameters.Split(' ');

                            bool isset = true; // flag saying this mode is being set. false if unset
                            int parameter = -1; // The index of the parameter the current mode points to

                            bool usermodechanged = false;


                            foreach (char c in modes.ToCharArray()) { // Loop each mode being changed

                                //Determine if the next modes in the string are to be set or unset. 
                                if (c == '+')
                                    isset = true;
                                else if (c == '-')
                                    isset = false;

                                if (match.Groups[2].Value == User.GetMe(this).Nick) // Usermode
                                {
                                }
                                else
                                {

                                    if (mode_user.Contains(c))
                                    { //If this is a user access mode
                                        parameter++;
                                        usermodechanged = true;
                                        User usr = User.GetUser(parametertoks[parameter], this);
                                        if (usr == null)
                                            Log.Error(this, "Null user \"{0}\"", parametertoks[parameter]);
                                        else
                                        {
                                            if (isset)
                                                usr.GetAccess(chan).AddMode(c);
                                            else
                                            {
                                                usr.GetAccess(chan).RemoveMode(c);
                                            }
                                        }

                                    }
                                    else if (mode_set.Contains(c))
                                    {
                                        if (isset)
                                            parameter++;
                                    }
                                    else if (mode_setunset.Contains(c))
                                    {
                                        parameter++;
                                    }
                                    else if (mode_list.Contains(c))
                                    {
                                        parameter++;
                                    }
                                }
                            }

                            if (_strictNames && usermodechanged)
                                Send("NAMES {0}", chan.Name);

                        }
                        catch (NullReferenceException) {
                            Console.WriteLine("NPE");
                            Console.WriteLine("Match: {0}", rChanMode.Match(line));
                            Console.WriteLine("Match: {0} UI: {1} U {2} C {3} modes {4} params {5} PREFIX {6}", rChanMode.Match(line), rChanMode.Match(line).Groups[1].Value.ParseUserInfo(), "ignoreme", Channel.GetChannel(rChanMode.Match(line).Groups[2].Value, this), rChanMode.Match(line).Groups[3].Value.Split(' ')[0], "ignoreme", Server.Prefix);
                        }

                    }

                    else if (Verbose) {
                        Console.WriteLine("Unhandled line: {0}", line);
                    }

                }
            }
        }
    }
}
